2090 2022-10-08 2023-02-11
前言:字节码编程是Java技术中一门较为有深度的技术,是一名高阶Java工程师必不可少的进阶知识,这次来较为系统的一起过下。
一、前言
本篇文章绝大部分参考自 bugstack 虫洞栈 - 《字节码编程,Byte-buddy篇》,作者:小傅哥,博客:https://bugstack.cn,有少许修改。
Byte Buddy is a code generation and manipulation library for creating and modifying Java classes during the runtime of a Java application and without the help of a compiler. Other than the code generation utilities that ship with the Java Class Library, Byte Buddy allows the creation of arbitrary classes and is not limited to implementing interfaces for the creation of runtime proxies. Furthermore, Byte Buddy offers a convenient API for changing classes either manually, using a Java agent or during a build.
Byte Buddy是一个代码生成和操作库,用于在Java应用程序运行时创建和修改Java类,而不需要编译器的帮助。除了Java类库附带的代码生成实用程序外,Byte Buddy允许创建任意类,并且不限于实现用于创建运行时代理的接口。此外,Byte Buddy提供了一个方便的API,可以手动、使用Java代理或在构建过程中更改类。
具体优势如下:
- 无需理解字节码指令,使用简单的API就能很容易地操作字节码,控制类和方法。
- 已支持Java 11,库轻量,仅取决于Java字节代码解析器库ASM的访问者API,它本身没有任何其他依赖项。
- 比起JDK动态代理、CGLIB、Javassist,Byte Buddy在性能上具有一定优势。
2015年10月,Byte Buddy被 Oracle 授予了 Duke's Choice大奖。该奖项对Byte Buddy的“ Java技术方面的巨大创新 ”表示赞赏。我们为获得此奖项感到非常荣幸,并感谢所有帮助Byte Buddy取得成功的用户以及其他所有人。我们真的很感激!
除了这些简单的介绍外,还可以通过官网:https://bytebuddy.net
,去了解更多关于 Byte Buddy
的内容。
二、Hello World
本节以官网的一个例子做演示说明。具体如下:
1、pom文件
implementation 'net.bytebuddy:byte-buddy:1.10.13'
implementation 'net.bytebuddy:byte-buddy-agent:1.10.13'
2、Java类
- 演示通过纯Java代码生成class对象并实现方法调用。
public class HelloWorld {
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
String helloWorld = new ByteBuddy()
// 继承自Object父类
.subclass(Object.class)
// 找到 toString 方法
.method(ElementMatchers.named("toString"))
// 拦截,并设定方法的返回值
.intercept(FixedValue.value("Hello World"))
// 生成class文件
.make()
// 加载
.load(HelloWorld.class.getClassLoader())
// 获取加载后class对象
.getLoaded()
// 实例化无参构造
.newInstance()
// 开始调用
.toString();
System.out.println("这一行是输出:" + helloWorld);
}
}
- 通过一个稍复杂的例子,来进一步认识bytebuddy。
public class HelloWorld2 {
public static void main1(String[] args) {
System.out.println("测试代理输出方法");
}
public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
DynamicType.Unloaded<?> dynamicType1 = new ByteBuddy()
.subclass(Object.class)
.name("site.xiaokui.bytebuddy.HelloWorld1")
.make();
// 输出类字节码
outputClazz(dynamicType1.getBytes(), "HelloWorld1");
DynamicType.Unloaded<?> dynamicType2 = new ByteBuddy()
.subclass(Object.class)
.name("site.xiaokui.bytebuddy.HelloWorld2")
// 定义方法,注意后面是 Modifier.PUBLIC + Modifier.STATIC
.defineMethod("main", void.class, Modifier.PUBLIC + Modifier.STATIC)
.withParameter(String[].class, "args")
.intercept(FixedValue.value("Hello World!"))
.make();
outputClazz(dynamicType2.getBytes(), "HelloWorld2");
DynamicType.Unloaded<?> dynamicType3 = new ByteBuddy()
.subclass(Object.class)
.name("site.xiaokui.bytebuddy.HelloWorld3")
.defineMethod("main1", void.class, Modifier.PUBLIC + Modifier.STATIC)
.withParameter(String[].class, "args")
// 委托,被委托的方法与需要与原方法有着一样的入参、出参、方法名,否则不能映射上
.intercept(MethodDelegation.to(HelloWorld2.class))
.make();
outputClazz(dynamicType3.getBytes(), "HelloWorld3");
// 加载类
Class<?> clazz = dynamicType3.load(HelloWorld2.class.getClassLoader())
.getLoaded();
// 反射调用,实际调用的还是HelloWorld2的main1方法
clazz.getMethod("main1", String[].class).invoke(clazz.newInstance(), (Object) new String[1]);
}
private static void outputClazz(byte[] bytes, String className) {
FileOutputStream out = null;
try {
String pathName = HelloWorld2.class.getResource("/").getPath() + className + ".class";
out = new FileOutputStream(pathName);
System.out.println("类输出路径:" + pathName);
out.write(bytes);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != out) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
3、生成的class文件
package site.xiaokui.bytebuddy;
public class HelloWorld1 {
public HelloWorld1() {
}
}
package site.xiaokui.bytebuddy;
public class HelloWorld2 {
public static void main(String[] args) {
String var10000 = "Hello World!";
}
public HelloWorld2() {
}
}
package org.itstack.demo.bytebuddy;
import site.xiaokui.bytebuddy.HelloWorld2;
public class HelloWorld3 {
public static void main1(String[] args) {
HelloWorld2.main1(var0);
}
public HelloWorld3() {
}
}
三、自定义监控
下面将通过一段代码,来实现对代理方法的监控,比如监控方法的执行耗时,出入参信息等。
1、被代理接口
public class BizController {
public String generateNum(String uid, String token) throws InterruptedException {
int num = new Random().nextInt(500);
Thread.sleep(num);
return "请求接口获取随机数:" + num;
}
}
2、Agent
实践发现,当有两个intercept方法同时存在时,只会有一个生效。这里简单测试了一下,第二个intercept总是会优先第一个intercept使用,跟定义顺序没有关系。
public class MonitorAgent {
/**
* 第一个,说明:@RuntimeType:定义运行时的目标方法,@SuperCall:用于调用父类版本的方法
*/
@RuntimeType
public static Object intercept(@SuperCall Callable<?> callable) throws Exception {
long start = System.currentTimeMillis();
try {
return callable.call();
} finally {
System.out.println("方法执行耗时:" + (System.currentTimeMillis() - start) + "ms");
}
}
/**
* 第二个,说明:@Origin,用于拦截原有方法,这样就可以获取到方法中的相关信息
*/
@RuntimeType
public static Object intercept(@Origin Method method, @AllArguments Object[] args, @Argument(0) Object arg0, @SuperCall Callable<?> callable) throws Exception {
long start = System.currentTimeMillis();
Object resObj = null;
try {
resObj = callable.call();
return resObj;
} finally {
System.out.println("方法名称:" + method.getName());
System.out.println("入参个数:" + method.getParameterCount());
System.out.println("入参类型:" + method.getParameterTypes()[0].getTypeName() + "、" + method.getParameterTypes()[1].getTypeName());
System.out.println("入参内容:" + arg0 + "、" + args[1]);
System.out.println("出参类型:" + method.getReturnType().getName());
System.out.println("出参结果:" + resObj);
System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms");
}
}
}
3、测试类
public class TestBizController {
public static void main(String[] args) throws Exception {
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
.subclass(BizController.class)
.method(ElementMatchers.named("generateNum"))
// 委托,被代理类的指定方法都会被委托类中的特定方法(如intercept)进行处理
.intercept(MethodDelegation.to(MonitorAgent.class))
.make();
// 加载类
Class<?> clazz = dynamicType.load(TestBizController.class.getClassLoader())
.getLoaded();
// 反射调用
clazz.getMethod("generateNum", String.class, String.class).invoke(clazz.newInstance(), "uid-hk", "token");
}
}
输出如下:
> Task :k-bytecode:TestBizController.main()
方法名称:generateNum
入参个数:2
入参类型:java.lang.String、java.lang.String
入参内容:uid、token
出参类型:java.lang.String
出参结果:请求接口获取随机数:103
方法耗时:108ms
四、稍微高级一点的例子
1、基础接口
public abstract class Repository<T> {
public abstract T queryData(int id);
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RpcGatewayClazz {
String clazzDesc() default "";
String alias() default "";
long timeOut() default 350;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RpcGatewayMethod {
String methodName() default "";
String methodDesc() default "";
}
2、拦截类
public class UserRepositoryInterceptor {
public static String intercept(@Origin Method method, @AllArguments Object[] arguments) {
return "小傅哥博客,查询文章数据:https://bugstack.cn/?id=" + arguments[0];
}
}
3、测试类
public class TestGateWay {
public static void main(String[] args) throws Exception {
// 生成含有注解的泛型实现字类
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
// 创建复杂类型的泛型注解
.subclass(TypeDescription.Generic.Builder.parameterizedType(DataRepository.class, String.class).build())
// 添加类信息包括地址
.name(DataRepository.class.getPackage().getName().concat(".").concat("UserRepositoryImpl"))
// 匹配处理的方法
.method(ElementMatchers.named("queryData"))
// 交给委托函数
.intercept(MethodDelegation.to(UserRepositoryInterceptor.class))
.annotateMethod(AnnotationDescription.Builder.ofType(RpcGatewayMethod.class)
.define("methodName", "queryData")
.define("methodDesc", "查询数据")
.build())
.annotateType(AnnotationDescription.Builder.ofType(RpcGatewayClazz.class)
.define("alias", "dataApi").define("clazzDesc", "查询数据信息")
.define("timeOut", 350L)
.build())
.make();
File file = new File(TestGateWay.class.getResource("/").getPath());
dynamicType.saveIn(file);
System.out.println("写入class值目标文件:" + file);
// 从目标文件夹下加载类信息
Class<DataRepository<String>> repositoryClass = (Class<DataRepository<String>>) Class.forName("site.xiaokui.bytebuddy.UserRepositoryImpl");
// 获取类注解
RpcGatewayClazz rpcGatewayClazz = repositoryClass.getAnnotation(RpcGatewayClazz.class);
System.out.println("RpcGatewayClazz.clazzDesc:" + rpcGatewayClazz.clazzDesc());
System.out.println("RpcGatewayClazz.alias:" + rpcGatewayClazz.alias());
System.out.println("RpcGatewayClazz.timeOut:" + rpcGatewayClazz.timeOut());
// 获取方法注解
RpcGatewayMethod rpcGatewayMethod = repositoryClass.getMethod("queryData", int.class).getAnnotation(RpcGatewayMethod.class);
System.out.println("RpcGatewayMethod.methodName:" + rpcGatewayMethod.methodName());
System.out.println("RpcGatewayMethod.methodDesc:" + rpcGatewayMethod.methodDesc());
// 实例化对象
DataRepository<String> repository = repositoryClass.newInstance();
// 测试输出
System.out.println(repository.queryData(10001));
}
}
输出如下:
> Task :k-bytecode:TestGateWay.main()
写入class值目标文件:/Users/huangkui/local_git/site.xiaokui/k-bytecode/build/classes/java/main
RpcGatewayClazz.clazzDesc:查询数据信息
RpcGatewayClazz.alias:dataApi
RpcGatewayClazz.timeOut:350
RpcGatewayMethod.methodName:queryData
RpcGatewayMethod.methodDesc:查询数据
小傅哥博客,查询文章数据:https://bugstack.cn/?id=10001
4、生成的class文件
package site.xiaokui.bytebuddy;
@RpcGatewayClazz(
alias = "dataApi",
clazzDesc = "查询数据信息",
timeOut = 350L
)
public class UserRepositoryImpl implements DataRepository<String> {
@RpcGatewayMethod(
methodDesc = "查询数据",
methodName = "queryData"
)
public String queryData(int id) {
return UserRepositoryInterceptor.intercept(cachedValue$BNVrKJZS$3ec9ur1, new Object[]{var1});
}
public UserRepositoryImpl() {
}
static {
cachedValue$BNVrKJZS$3ec9ur1 = DataRepository.class.getMethod("queryData", Integer.TYPE);
}
}
对byte-buddy的认识就先到这里吧,后面如果有进一步的深入了解,再来更新,估计不会等太久了,因为最近确实是需要了解这一块东西。
五、源码地址
总访问次数: 106次, 一般般帅 创建于 2022-10-08, 最后更新于 2023-02-11
欢迎关注微信公众号,第一时间掌握最新动态!